using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;

using Google.ProtocolBuffers;

using VisualGTest.Message;

namespace VisualGTest.Binding
{
    /// <summary>
    /// Proxy for executing the gtest-runner.
    /// </summary>
    class Runner
    {
        public static readonly int DEFAULT_LIST_TESTS_TIMEOUT = 5000;
        public static readonly int DEFAULT_PIPE_CONNECT_TIMEOUT = 5000;

        private static int pipeIndex = 0;

        #region property backing variables
        private string _gtestRunnerPath;
        private string _testDllPath;
        private int _listTestsTimeout;
        private int _pipeConnectTimeout;
        #endregion

        #region properties
        /// <summary>
        /// path to the gtest-runner executable
        /// </summary>
        public string GTestRunnerPath
        {
            get { return _gtestRunnerPath; }
            set { _gtestRunnerPath = value; }
        }

        /// <summary>
        /// path to the DLL containing the tests
        /// </summary>
        public string TestDllPath
        {
            get { return _testDllPath; }
            set { _testDllPath = value; }
        }

        /// <summary>
        /// the amount of time to wait for the test listing gtest-runner
        /// process to finish, in milliseconds. Defaults to
        /// DEFAULT_LIST_TESTS_TIMEOUT .
        /// </summary>
        public int ListTestsTimeout
        {
            get { return _listTestsTimeout; }
            set { _listTestsTimeout = value; }
        }
            
        /// <summary>
        /// the amount of time to wait for a connection from the gtest-runner
        /// process, in milliseconds. Defaults to DEFAULT_PIPE_CONNECT_TIMEOUT.
        /// </summary>
        public int PipeConnectTimeout
        {
            get { return _pipeConnectTimeout; }
            set { _pipeConnectTimeout = value; }
        }
        #endregion

        private IProcessFactory m_processFactory;
        private IPipeStreamFactory m_pipeFactory;
        private FileCapsule m_fileCapsule;

        /// <summary></summary>
        /// <param name="pipeFactory">the factory producing the pipe server
        /// stream used to read from the gtest-runner process</param>
        /// <param name="FileCapsule">used for file operations</param>
        public Runner(IProcessFactory processFactory, IPipeStreamFactory pipeFactory, FileCapsule fileCapsule)
        {
            m_processFactory = processFactory;
            m_pipeFactory = pipeFactory;
            m_fileCapsule = fileCapsule;

            this.ListTestsTimeout = DEFAULT_LIST_TESTS_TIMEOUT;
            this.PipeConnectTimeout = DEFAULT_PIPE_CONNECT_TIMEOUT;
        }

        /// <summary>
        /// Returns a list of tests defined in the DLL at dllPath. If no tests
        /// are defined, returns an empty list. If the operation failed, returns
        /// null. This is a synchronous, blocking operation.
        /// </summary>
        /// <returns></returns>
        public List<TestId> ListTests()
        {
            if (! SetupIsValid())
            {
                return null;
            }

            List<TestId> tests = new List<TestId>();
            ProcessStartInfo startInfo = new ProcessStartInfo();

            startInfo.FileName = GTestRunnerPath;
            startInfo.Arguments = BuildListTestsArgs();
            startInfo.RedirectStandardOutput = true;
            startInfo.UseShellExecute = false;

            using (IProcess p = m_processFactory.CreateProcess(startInfo))
            {
                p.Start();

                string outputLine;
                string currentTestCaseName = "";
                string testName;
                char[] dot = new char[] { '.' };

                while ((outputLine = p.StandardOutput.ReadLine()) != null) {
                    if (! outputLine.StartsWith(" "))
                    {
                        // the test case name is printed with a trailing dot
                        currentTestCaseName = outputLine.TrimEnd(dot);
                    }
                    else
                    {
                        testName = outputLine.Trim();
                        tests.Add(new TestId(currentTestCaseName, testName));
                    }
                }

                if (! p.WaitForExit(this.ListTestsTimeout))
                {
                    // TODO - log an error
                    return null;
                }
            }

            return tests;
        }

        /// <summary>
        /// Runs the tests defined in the test DLL that satisfy the specified
        /// test name filters, invoking the callbacks on the specified
        /// listener when results are received.
        /// </summary>
        /// <remarks>
        /// A pipe connection is used to read from the gtest process. The
        /// connection to the pipe is established more-or-less synchronously,
        /// while the actual running of the tests is asynchronous.
        /// </remarks>
        /// <returns>a state object used for asynchronous calls within this
        /// method. This object can be passed to EndTestRun() to abort the
        /// test run. If the setup is invalid, returns null.</returns>
        /// <exception cref="System.TimeoutException">the pipe did not connect
        /// within PipeConnectTimeout</exception>
        /// <exception cref="System.OperationCanceledException">the pipe
        /// connection closed prematurely</exception>
        public IAsyncResult BeginTestRun(List<string> testFilters, ITestEventListener listener, AsyncCallback asyncCallback)
        {
            // TODO - add some state to prevent re-entrance

            if (! SetupIsValid())
            {
                // TODO - give some useful error information, or throw exception, etc.
                return null;
            }

            string pipeName = NewPipeName();

            ProcessStartInfo gtestStartInfo = new ProcessStartInfo(
                GTestRunnerPath, BuildRunTestsArgs(pipeName, testFilters));

            // normally, we'd use a using block to dispose of the pipe stream.
            // However, because we're using the pipe asynchronously, we have
            // to handle disposal manually.
            NamedPipeServerStream pipeServer = m_pipeFactory.CreateServer(pipeName);
            IProcess gtest = m_processFactory.CreateProcess(gtestStartInfo);
            IAsyncResult asyncResult = pipeServer.BeginWaitForConnection(null, null);

            // start the process expected to connect as the pipe client
            gtest.Start();

            Console.WriteLine("Waiting for client connection...");

            // wait for the connection
            if (! asyncResult.AsyncWaitHandle.WaitOne(this.PipeConnectTimeout))
            {
                // abandon the asynchronous operation
                throw new TimeoutException("Timed out waiting for gtest-runner process to connect.");
            }

            Console.WriteLine("Successfully connected to gtest-runner process!");

            // will throw OperationCanceledException if the pipe stream is
            // closed prematurely
            pipeServer.EndWaitForConnection(asyncResult);

            TestRunAsyncState state = new TestRunAsyncState(pipeServer, gtest, listener);

            asyncResult = new TestRunAsyncResult(asyncCallback, state);

            ThreadPool.QueueUserWorkItem(new WaitCallback(TestRunProc), asyncResult);

            return asyncResult;
        }

        private void TestRunProc(object state)
        {
            TestRunAsyncResult asyncResult = state as TestRunAsyncResult;
            TestRunAsyncState asyncState = asyncResult.AsyncState as TestRunAsyncState;
            NamedPipeServerStream pipeServer = asyncState.PipeServer;
            ITestEventListener listener = asyncState.Listener;

            PipeMessageReader reader = new PipeMessageReader();
            byte[] messageBytes;

            try
            {
                while (pipeServer.IsConnected)
                {
                    // this should block synchronously; no busy waiting
                    messageBytes = reader.ReadMessageBytes(pipeServer);

                    if (messageBytes == null)
                    {
                        // TODO - throw some sort of exception and/or log!
                        break;
                    }

                    if (messageBytes.Length == 0)
                    {
                        // wait for more content, or a disconnect
                        Thread.Sleep(100);
                        continue;
                    }

                    Event testEvent = Event.ParseFrom(messageBytes);

                    switch (testEvent.EventType)
                    {
                        case Event.Types.EventType.TestProgramStart:
                            listener.OnTestProgramStart(testEvent.UnitTest);
                            break;

                        case Event.Types.EventType.TestIterationStart:
                            listener.OnTestIterationStart(testEvent.UnitTest);
                            break;

                        case Event.Types.EventType.EnvironmentsSetUpStart:
                            listener.OnEnvironmentSetUpStart(testEvent.UnitTest);
                            break;

                        case Event.Types.EventType.EnvironmentsSetUpEnd:
                            listener.OnEnvironmentSetUpEnd(testEvent.UnitTest);
                            break;

                        case Event.Types.EventType.TestCaseStart:
                            listener.OnTestCaseStart(testEvent.TestCase);
                            break;

                        case Event.Types.EventType.TestStart:
                            listener.OnTestStart(testEvent.TestInfo);
                            break;

                        case Event.Types.EventType.TestPartResult:
                            listener.OnTestPartResult(testEvent.TestPartResult);
                            break;

                        case Event.Types.EventType.TestEnd:
                            listener.OnTestEnd(testEvent.TestInfo);
                            break;

                        case Event.Types.EventType.TestCaseEnd:
                            listener.OnTestCaseEnd(testEvent.TestCase);
                            break;

                        case Event.Types.EventType.EnvironmentsTearDownStart:
                            listener.OnEnvironmentTearDownStart(testEvent.UnitTest);
                            break;

                        case Event.Types.EventType.EnvironmentsTearDownEnd:
                            listener.OnEnvironmentTearDownEnd(testEvent.UnitTest);
                            break;

                        case Event.Types.EventType.TestIterationEnd:
                            listener.OnTestIterationEnd(testEvent.UnitTest);
                            break;

                        case Event.Types.EventType.TestProgramEnd:
                            listener.OnTestProgramEnd(testEvent.UnitTest);
                            break;
                    }
                }
            }
            catch (Exception ex)
            {
                listener.OnTestRunError(ex);
            }

            asyncResult.CallUserCallback();
        }

        /// <summary>
        /// This should be called once for every invocation of BeginTestRun().
        /// </summary>
        /// <param name="asyncResult">the object that was returned by the call
        /// to BeginTestRun() to initiate the test run.</param>
        public void EndTestRun(IAsyncResult asyncResult)
        {
            // TODO - provide some way to forcefully terminate the test run

            if (asyncResult == null)
            {
                throw new ArgumentNullException("asyncResult");
            }

            TestRunAsyncResult result = asyncResult as TestRunAsyncResult;

            if (result == null)
            {
                throw new ArgumentException("Wrong asyncResult");
            }

            WaitHandle waitHandle = asyncResult.AsyncWaitHandle;

            try
            {
                waitHandle.WaitOne();
            }
            finally
            {
                waitHandle.Close();
            }

            TestRunAsyncState state = asyncResult.AsyncState as TestRunAsyncState;
            IProcess gtest = state.GTest;
            NamedPipeServerStream pipeServer = state.PipeServer;

            try
            {
                gtest.Kill();
            }
            catch (Exception)
            {
            	// process may have already terminated
            }

            gtest.Dispose();

            // closing also disposes
            pipeServer.Close();
        }

        /// <summary>
        /// Returns whether the gtest-runner can be executed with the current
        /// settings.
        /// </summary>
        private bool SetupIsValid()
        {
            if (! m_fileCapsule.Exists(GTestRunnerPath))
            {
                Console.WriteLine("gtest runner file does not exist!");
                return false;
            }

            if (! m_fileCapsule.Exists(TestDllPath))
            {
                Console.WriteLine("DLL file does not exist!");
                return false;
            }

            return true;
        }

        private string BuildListTestsArgs()
        {
            return string.Format("--testlib={0} --gtest_list_tests", TestDllPath);
        }

        /// <summary>
        /// Returns a new, process-unique pipe name. Thread-safe.
        /// </summary>
        /// <returns></returns>
        private string NewPipeName()
        {
            return string.Format("RunnerPipe{0}",
                Interlocked.Increment(ref pipeIndex));
        }

        private string BuildRunTestsArgs(string pipeName, List<string> testFilters)
        {
            string args = string.Format("--testlib={0} --pipename={1} ",
                TestDllPath, pipeName);

            if (testFilters != null && testFilters.Count > 0)
            {
                args += "--gtest_filter=" + string.Join(":", testFilters.ToArray());
            }

            return args;
        }
    }
}
